/*
 * Copyright (c) 2010-2013 Ruben Chakhmakhchyan. All rights reserved.
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Eclipse Public License
 * Version 1.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://opensource.org/licenses/EPL-1.0 and read it before using this file.
 */

#ifndef _ADDON

#include "net.h"

#include "commontypes.h"
#include "strings.h"

#include <stdio.h>
#include <stdlib.h>

// winsock
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>

// ssl
#ifdef PLUGIN_OPENSSL
#include <openssl/crypto.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/err.h> 
#else
#include <winhttp.h>
#endif

#include "result_codes.h"

#include "image_list.h"
#include "convert.h"

// send/receive buffer sizes
#define HTTP_SEND_BUFFER 65536
#define HTTP_RECEIVE_BUFFER 1024


class MemoryStream
{
private:

    char* m_buffer;
    char* m_ptr;
    size_t m_bufferSize;
    size_t m_size;

public:

    MemoryStream( size_t capacity )
        : m_size( 0 )
    {
        m_bufferSize = capacity;
        m_buffer = new char[ capacity ];
        ::memset( m_buffer, 0, capacity );
        m_ptr = m_buffer;
    }

    MemoryStream()
        : m_size( 0 ),
        m_buffer( NULL ),
        m_ptr( NULL ),
        m_bufferSize( 0 )
    {
    }

    ~MemoryStream()
    {
        if ( m_buffer ) delete m_buffer;
    }

    void* getBuffer() { return m_buffer; }
    size_t getSize() { return m_size; }

    MemoryStream& write( char* c, bool overwrite = false )
    {
        size_t len = String::GetLength( c );
        write( c, len, overwrite );

        return *this;
    }

    MemoryStream& writeWide( char* c, bool overwrite = false )
    {
        size_t len = String::GetLength( c );

        wchar_t* wc = String::ToWchar( c );

        write( wc, len << 1, overwrite );

        return *this;
    }

    MemoryStream& write( wchar_t* wc, bool overwrite = false )
    {
        size_t len = String::GetLength( wc );

        write( wc, len << 1, overwrite );

        return *this;
    }

    MemoryStream& write( char c )
    {
        write( &c, 1, false );

        return *this;
    }

    MemoryStream& write( void* pdata, size_t length, bool overwrite = false )
    {
        ::memcpy( m_ptr, pdata, length );

        if ( !overwrite )
            m_size += length;

        m_ptr = m_buffer + m_size;

        return *this;
    }

    MemoryStream& write( int value )
    {
        char buffer[ 16 ] = { 0 };

        Convert::ToString( buffer, value );

        write( buffer );

        return *this;
    }

    MemoryStream& setPosition( size_t value )
    {
        m_ptr = m_buffer + value;
        m_size = value;

        return *this;
    }
};

#define FORMDATABOUNDARYA "------------------4lfq2b3j6fb9"
#define FORMDATABOUNDARYW L"------------------4lfq2b3j6fb9"

int getIp( char* url, unsigned long& ip );
int getHttpResponseCode( char* buffer, size_t length );


Net::Net()
{
}

Net::~Net()
{
}

int getHttpResponseCode( char* buffer, size_t length )
{
    return ( ::strstr( buffer, "HTTP/1.1 " ) != buffer || ::strstr( buffer, "HTTP/1.0 " ) != buffer ) ? -1 : Convert::ToInt( buffer + 9 );
}

int getIp( char* url, unsigned long& ip )
{
    ip = 0;
    DWORD dwRetval;

    struct addrinfo *result = NULL;
    struct addrinfo *ptr = NULL;
    struct addrinfo hints;

    struct sockaddr_in  *sockaddr_ipv4;
    struct sockaddr_in6 *sockaddr_ipv6;
    //	struct sockaddr* sockaddr_ip;

    // Setup the hints address info structure which is passed to the getaddrinfo() function
    ZeroMemory( &hints, sizeof( hints ) );
    hints.ai_family = AF_INET;//AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    // Call getaddrinfo(). If the call succeeds, the result variable will hold a linked list of addrinfo structures containing response information
    dwRetval = ::getaddrinfo( url, "0", &hints, &result );
    if ( dwRetval != 0 )
    {
        // getaddrinfo failed with error: dwRetval
        return 1;
    }

    // getaddrinfo returned success

    // retrieve each address and print out the hex bytes
    int i = 1;
    for( ptr = result; ptr != NULL; ptr = ptr->ai_next, i++ )
    {
        // flags:
        //ptr->ai_flags

        // family
        switch ( ptr->ai_family )
        {
        case AF_UNSPEC:
            return 2;

        case AF_INET:
            sockaddr_ipv4 = ( struct sockaddr_in * ) ptr->ai_addr; // ( sockaddr_in6 * ) for IPv6
            ip = sockaddr_ipv4->sin_addr.S_un.S_addr;
            break;
        case AF_INET6:
            sockaddr_ipv6 = ( struct sockaddr_in6 * ) ptr->ai_addr;
            // TODO: *ip = ... ( v6 )

        default:
            return 2;
        }

        if ( ip )
            break;

        // socket type: 0( unspecified ), SOCK_STREAM...
        //ptr->ai_socktype

        // protocol: 0( unspecified ), IPPROTO_TCP, ...
        //ptr->ai_protocol

        // length of this sockaddr:
        //ptr->ai_addrlen;

        // canonical name:
        //ptr->ai_canonname );
    }

    ::freeaddrinfo( result );
    return 0;
}

#ifndef PLUGIN_OPENSSL

ResultCode httpSendRequest( HINTERNET hRequest, byte_t* data, uint_t dataLength, char* mimeType, char* filename )
{
    DWORD errorCode = 0;

    //char abuf[ 16 ] = { 0 };

    if ( !filename )
    {
        filename = "image";
    }

    MemoryStream ms1( 2048 );
    char* boundary = FORMDATABOUNDARYA;
    wchar_t* boundaryL = FORMDATABOUNDARYW;

    ms1.write( L"Connection: Keep-Alive\r\n" );

    ms1.write( L"Content-Type: multipart/form-data; boundary=" ).write( boundaryL ).write( L"\r\n" );

    //TODO: Referer must be specified

    MemoryStream ms2( dataLength + 2048 );

    ms2.write( "\r\n" );

    //size_t savepoint2 = ms2.getSize();

    ms2.write( "--" ).write( boundary ).write( "\r\n" )
        .write( "Content-Disposition: form-data; name=\"file\"; filename=\"" ).write( filename ).write( "\"\r\n" )
        .write( "Content-Type: " ).write( mimeType ).write( "\r\n" ) //application/octet-stream
        .write( "\r\n" )
        .write( data, dataLength ).write( "\r\n" )
        .write( "--" ).write( boundary ).write( "--" );

    //size_t cl = ms2.getSize() - savepoint2;
    //ms1.setPosition( savepoint ).writeWide( _itoa( cl, abuf, 10 ), true );

    size_t fileContentSize = ms2.getSize();
    byte_t* pBuffer = ( byte_t* ) ms2.getBuffer();


    // set https options
    //if ( https )
    {
        DWORD options = SECURITY_FLAG_IGNORE_CERT_CN_INVALID | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_UNKNOWN_CA | SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE;

        if ( !::WinHttpSetOption( hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &options, sizeof( DWORD ) ) )
        {
            errorCode = ::GetLastError();
            ::printf( "WinHttpSetOption failed %d\n", errorCode );
            return ResultCode::NetConfigSession;
        }
    }

    // send mock request
    BOOL bRet = 0;

    // send request

    //DWORD dwBytesWritten;

    if ( !::WinHttpSendRequest( hRequest, ( wchar_t* ) ms1.getBuffer(), ms1.getSize() >> 1, NULL, 0, fileContentSize, 10 ) )
    {
        errorCode = ::GetLastError();
        ::printf( "Error on WinHttpSendRequest %d\n", errorCode );
        return ResultCode::NetSend;
    }

    //// get a handle to the certificate
    //   bRet = WinHttpQueryOption(
    //       hRequest,
    //       WINHTTP_OPTION_SERVER_CERT_CONTEXT,
    //       &pCert,
    //       &dwLen
    //   );

    // write form data

    bRet = TRUE;
    ulong_t bytesSent = 0;
    ulong_t totalBytesSent = 0;
    ulong_t sendSize = fileContentSize;

    while ( bRet )
    {
        size_t ds = sendSize - totalBytesSent;

        if ( bRet = ::WinHttpWriteData( hRequest, pBuffer + totalBytesSent, ds < HTTP_SEND_BUFFER ? ds : HTTP_SEND_BUFFER, &bytesSent ) )
            ::printf( "\r%d bytes sent.", bytesSent );

        if ( bytesSent == 0 )
        {
            ::printf( "Error on WinHttpWriteData %d\n", ::GetLastError() );
            break;
        }
        else if ( bytesSent > 0 )
        {
            totalBytesSent += bytesSent;
            if ( totalBytesSent == sendSize )
                break;
            else if ( totalBytesSent > sendSize )
            {
                throw "Send size mismatch!";
            }
        }
    }

    if ( !bRet )
    {
        ::printf( "\nError on InternetWriteFile %lu\n", ::GetLastError() );
        return ResultCode::NetSend;
    }

    if( !::WinHttpReceiveResponse( hRequest, NULL ) )
    {
        ::printf( "Error on HttpEndRequest %lu \n", ::GetLastError());
        return ResultCode::NetReceive;
    }

    return ResultCode::Success;
}

uint_t digitsInOrdinalSequence( uint_t number )
{
    uint_t i = 1, i1 = 0, digits = 1;
    uint_t result = 0;
    uint_t base = 0;

    while ( true )
    {
        i1 = i * 10;
        if ( number < i1 )
        {
            result = base + ( ( number - i + 1 ) * digits );
            return result;
        }
        else
        {
            base = base + ( 9 * i * digits );
            i = i1;
            digits++;
        }
    }
    return 0;
}

ResultCode httpSendRequest( HINTERNET hRequest, ImageList* images, char* mimeType, List<char*>* filenames )
{
    DWORD errorCode = 0;

    byte_t* data = NULL;
    uint_t dataLength = 0;
    char* defaultFilename = "image";

    /*if ( !filename )
    {
    filename = "image";
    }*/

    List<char*>& filenameList = *filenames;

    char* boundary = FORMDATABOUNDARYA;
    wchar_t* boundaryL = FORMDATABOUNDARYW;
    uint_t boundaryLength = String::GetLength( boundary );

    uint_t mimeTypeLength = String::GetLength( mimeType );
    uint_t imageCount = images->getCount();

    uint_t totalDataLength = 0, maxDataLength = 0;
    int count = images->getCount();
    size_t fileContentSize = 0;

    for ( int i = 0; i < count; i++ )
    {
        images->getItem( i, data, dataLength );

        totalDataLength += dataLength;
        if ( maxDataLength < dataLength ) maxDataLength = dataLength;

        fileContentSize += ( 2 
            + ( 2 + boundaryLength + 2 ) 
            + ( 43 + 0 + 13 + String::GetLength( filenameList[ 0 ] ) + 3 ) 
            + ( 14 + mimeTypeLength + 2 )
            + ( 2 )
            + ( dataLength + 2 ) 
            + ( 2 + boundaryLength ) );
    }
    fileContentSize += ::digitsInOrdinalSequence( imageCount ) + 2;


    // set https options
    //if ( https )
    {
        DWORD options = SECURITY_FLAG_IGNORE_CERT_CN_INVALID | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_UNKNOWN_CA | SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE;

        if ( !::WinHttpSetOption( hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &options, sizeof( DWORD ) ) )
        {
            errorCode = ::GetLastError();
            ::printf( "WinHttpSetOption failed %d\n", errorCode );
            return ResultCode::NetConfigSession;
        }
    }

    // send mock request

    MemoryStream ms1( 2048 );

    ms1.write( L"Connection: Keep-Alive\r\n" );

    ms1.write( L"Content-Type: multipart/form-data; boundary=" ).write( boundaryL ).write( L"\r\n" );

    //TODO: Referer must be specified

    BOOL bRet = 0;


    // send request

    DWORD dwBytesWritten = 0;

    if ( !::WinHttpSendRequest( hRequest, ( wchar_t* ) ms1.getBuffer(), ms1.getSize() >> 1, NULL, 0, fileContentSize, 10 ) )
    {
        errorCode = ::GetLastError();
        ::printf( "Error on WinHttpSendRequest %d\n", errorCode );
        return ResultCode::NetSend;
    }

    //// get a handle to the certificate
    //   bRet = WinHttpQueryOption(
    //       hRequest,
    //       WINHTTP_OPTION_SERVER_CERT_CONTEXT,
    //       &pCert,
    //       &dwLen
    //   );

    // write form data

    MemoryStream ms2( maxDataLength + 2048 );

    for ( int i = 1; i <= count; i++ )
    {
        images->getItem( i - 1, data, dataLength );

        bRet = TRUE;
        ulong_t bytesSent = 0;
        ulong_t totalBytesSent = 0;

        ms2.write( "\r\n" );
        ms2.write( "--" ).write( boundary ).write( "\r\n" )
            .write( "Content-Disposition: form-data; name=\"file_" ).write( i ).write( "\"; filename=\"" ).write( ( *filenames )[ i - 1 ] ).write( "\"\r\n" )
            .write( "Content-Type: " ).write( mimeType ).write( "\r\n" ) //application/octet-stream
            .write( "\r\n" )
            .write( data, dataLength ).write( "\r\n" )
            .write( "--" ).write( boundary );//.write( "--" );

        if ( i == count )
        {
            ms2.write( "--" );
        }

        byte_t* pBuffer = ( byte_t* ) ms2.getBuffer();
        ulong_t sendSize = ms2.getSize();

        while ( bRet )
        {
            size_t ds = sendSize - totalBytesSent;

            if ( bRet = WinHttpWriteData( hRequest, pBuffer + totalBytesSent, ds < HTTP_SEND_BUFFER ? ds : HTTP_SEND_BUFFER, &bytesSent ) )
                ::printf( "\r%d bytes sent.", bytesSent );

            if ( !bRet || bytesSent == 0 )
            {
                ::printf( "Error on WinHttpWriteData %d\n", ::GetLastError() );
                throw "0 bytes sent!";
                //break;
            }
            else if ( bytesSent > 0 )
            {
                totalBytesSent += bytesSent;
                if ( totalBytesSent == sendSize )
                    break;
                else if ( totalBytesSent > sendSize )
                {
                    throw "Send size mismatch!";
                }
            }
        }

        ms2.setPosition( 0 );

        if ( !bRet )
        {
            ::printf( "\nError on InternetWriteFile %lu\n", ::GetLastError() );
            return ResultCode::NetSend;
        }
    }

    if( !::WinHttpReceiveResponse( hRequest, NULL ) )
    {
        ::printf( "Error on HttpEndRequest %lu \n", ::GetLastError());
        return ResultCode::NetReceive;
    }

    return ResultCode::Success;
}

ResultCode httpSendRequest1( HINTERNET hRequest, ImageList* list, byte_t* data, uint_t dataLength, char* mimeType, List<char*>* filenames )
{
    if ( list )
    {
        return httpSendRequest( hRequest, list, mimeType, filenames );
    }

    return httpSendRequest( hRequest, data, dataLength, mimeType, filenames ? ( *filenames )[ 0 ] : NULL );
}

#endif

VOID CALLBACK httpCallback( HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength )
{
    int i = 0;

    if ( dwInternetStatus == WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE )
    {
        int j = 0;
    }
}

ResultCode Net::httpPost( const char* urlHost, uint16_t port, const char* urlPath, bool secure, char* userAgent, ImageList* list, void* data, size_t dataLength, char* mimeType, List<char*>* filenames )
{
    ResultCode result = ResultCode::Success;

    try
    {
        ulong_t errorCode = 0;

        //result = splitUrl( url, urlDesc );
        //if ( result )
        //{
        //	throw result;
        //}

        wchar_t* host = String::ToWchar( urlHost );
        wchar_t* path = String::ToWchar( urlPath );
        wchar_t* agent = String::ToWchar( userAgent );

        HINTERNET hSession = ::WinHttpOpen( agent, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0 );

        // set status callback
        /*
        if ( hSession )
        {
        // Install the status callback function.
        WINHTTP_STATUS_CALLBACK callbackStatus = WinHttpSetStatusCallback( hSession, ( WINHTTP_STATUS_CALLBACK ) httpCallback, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, NULL );

        int h = 0;
        // Place additional code here.

        // When finished, release the HINTERNET handle.
        //WinHttpCloseHandle(hSession);
        }
        else
        {
        printf( "Failed to open session\n" );
        result = 1;
        }
        */

        //WinHttpSetTimeouts( hSession, // todo: configure resolve/connect/send/receive timeouts

        //HINTERNET hConnect = WinHttpConnect( hSession, L"localhost", /*INTERNET_DEFAULT_HTTP_PORT*/ 2544, 0 );

        if ( result != ResultCode::Success )
        {
            //uint16_t port = urlDesc.port;
            bool isHttps = secure; // == PROTOCOL_HTTPS;

            if ( /*urlDesc.protocol > 0 &&*/ !port )
            {
                port = isHttps ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT;
            }

            HINTERNET hConnect = ::WinHttpConnect( hSession, host, port, 0 );

            if ( !hConnect )
                ::printf( "Failed to connect\n" );
            else
            {
                //HINTERNET hRequest = WinHttpOpenRequest( hConnect, L"POST", L"/web/default.aspx", NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_REFRESH );
                HINTERNET hRequest = ::WinHttpOpenRequest( hConnect, L"POST", path, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_REFRESH | ( isHttps ? WINHTTP_FLAG_SECURE : 0 ) );
                //WINHTTP_FLAG_SECURE_PROTOCOL_ALL

                if ( !hRequest )
                    ::printf( "Failed to open request handle\n" );
                else
                {
                    ResultCode sendResult = sendResult = ::httpSendRequest1( hRequest, list, ( byte_t* ) data, dataLength, mimeType, filenames );

                    if ( sendResult != ResultCode::Success )
                    {
                        ulong_t statusCode = 0, headerBufferSize = sizeof( ulong_t );//WINHTTP_QUERY_RAW_HEADERS_CRLF
                        //if ( !WinHttpQueryHeaders( hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, NULL, WINHTTP_NO_OUTPUT_BUFFER, &headerBufferSize, WINHTTP_NO_HEADER_INDEX ) )
                        //{
                        //	if ( GetLastError() != ERROR_INSUFFICIENT_BUFFER )
                        //	{
                        //		// todo: 
                        //	}
                        //}

                        //char buffer[ 10480 ] = { 0 };
                        //headerBufferSize = sizeof(;
                        if ( ::WinHttpQueryHeaders( hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &headerBufferSize, WINHTTP_NO_HEADER_INDEX ) )
                        {
                            if ( statusCode == HTTP_STATUS_OK )
                            {
                                char pcBuffer[ HTTP_RECEIVE_BUFFER ];
                                DWORD dwBytesRead;

                                ::printf( "\nThe following was returned by the server:\n" );

                                do
                                {
                                    dwBytesRead = 0;

                                    if ( ::WinHttpReadData( hRequest, pcBuffer, HTTP_RECEIVE_BUFFER - 1, &dwBytesRead ) )
                                    {
                                        pcBuffer[ dwBytesRead ] = 0x00; // null-terminate buffer

                                        ::printf( "%s", pcBuffer );
                                    }
                                    else
                                        ::printf( "\nInternetReadFile failed" );
                                }
                                while ( dwBytesRead > 0 );

                                ::printf( "\n" );
                            }
                            else
                            {
                                result = ResultCode::NetSend;
                            }
                        }
                        else
                        {
                            result = ResultCode::NetReceive;
                            errorCode = ::GetLastError();
                        }
                    }
                    else
                    {
                        result = sendResult;
                    }

                    if ( !::WinHttpCloseHandle( hRequest ) )
                        ::printf( "Failed to close Request handle\n" );
                }

                if ( !::WinHttpCloseHandle( hConnect ) )
                    ::printf( "Failed to close Connect handle\n" );
            }

            if( ::WinHttpCloseHandle( hSession ) == FALSE )
                ::printf( "Failed to close Session handle\n" );

            ::printf( "\nFinished.\n" );
        }

        if ( host ) delete host;
        if ( path ) delete path;
        if ( agent ) delete agent;
    }
    catch( int ex )
    {
        result = ( ResultCode ) ex;
    }

    return result;
}

#endif